"""
Error recovery utilities for the data preprocessing pipeline.

This module provides graceful error recovery mechanisms, user notification systems,
and fallback strategies for handling various types of failures.
"""

import logging
import traceback
import time
from typing import Optional, Callable, Any, Dict, List, Tuple
from functools import wraps
from contextlib import contextmanager

from .exceptions import (
    PreprocessingPipelineError, FileOperationError, DataValidationError,
    ConfigurationError, PreprocessorError, ProcessingError, OutputError,
    ResourceError, UserCancellationError
)


class ErrorRecoveryManager:
    """
    Manages error recovery strategies and user notifications.
    
    This class provides centralized error handling, recovery mechanisms,
    and user-friendly error reporting for the preprocessing pipeline.
    """
    
    def __init__(self, logger: Optional[logging.Logger] = None):
        """
        Initialize the error recovery manager.
        
        Args:
            logger (Optional[logging.Logger]): Logger instance for error reporting
        """
        self.logger = logger or logging.getLogger(__name__)
        self.error_history: List[Dict[str, Any]] = []
        self.recovery_strategies: Dict[str, Callable] = {}
        self.user_notification_callback: Optional[Callable[[str, str], None]] = None
        
        # Configure default recovery strategies
        self._setup_default_recovery_strategies()
    
    def _setup_default_recovery_strategies(self):
        """Set up default recovery strategies for common error types."""
        self.recovery_strategies.update({
            'FileOperationError': self._recover_file_operation_error,
            'DataValidationError': self._recover_data_validation_error,
            'ConfigurationError': self._recover_configuration_error,
            'PreprocessorError': self._recover_preprocessor_error,
            'ProcessingError': self._recover_processing_error,
            'OutputError': self._recover_output_error,
            'ResourceError': self._recover_resource_error
        })
    
    def set_user_notification_callback(self, callback: Callable[[str, str], None]):
        """
        Set callback function for user notifications.
        
        Args:
            callback: Function that takes (title, message) and displays to user
        """
        self.user_notification_callback = callback
    
    def handle_error(self, error: Exception, context: Optional[str] = None,
                    attempt_recovery: bool = True) -> Tuple[bool, Optional[Any]]:
        """
        Handle an error with optional recovery attempt.
        
        Args:
            error (Exception): The error to handle
            context (Optional[str]): Context where the error occurred
            attempt_recovery (bool): Whether to attempt automatic recovery
            
        Returns:
            Tuple[bool, Optional[Any]]: (recovery_successful, recovery_result)
        """
        error_info = {
            'timestamp': time.time(),
            'error_type': type(error).__name__,
            'error_message': str(error),
            'context': context,
            'traceback': traceback.format_exc(),
            'recovery_attempted': attempt_recovery,
            'recovery_successful': False
        }
        
        # Log the error
        self.logger.error(
            f"Error in {context or 'unknown context'}: {type(error).__name__}: {error}"
        )
        self.logger.debug(f"Full traceback: {traceback.format_exc()}")
        
        # Notify user if callback is set
        if self.user_notification_callback:
            user_message = self._get_user_friendly_message(error, context)
            self.user_notification_callback("Error Occurred", user_message)
        
        recovery_result = None
        if attempt_recovery:
            recovery_successful, recovery_result = self._attempt_recovery(error, context)
            error_info['recovery_successful'] = recovery_successful
            
            if recovery_successful:
                self.logger.info(f"Successfully recovered from {type(error).__name__}")
            else:
                self.logger.warning(f"Failed to recover from {type(error).__name__}")
        
        # Store error in history
        self.error_history.append(error_info)
        
        return error_info['recovery_successful'], recovery_result
    
    def _attempt_recovery(self, error: Exception, context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """
        Attempt to recover from an error using registered strategies.
        
        Args:
            error (Exception): The error to recover from
            context (Optional[str]): Context where the error occurred
            
        Returns:
            Tuple[bool, Optional[Any]]: (success, recovery_result)
        """
        error_type = type(error).__name__
        
        if error_type in self.recovery_strategies:
            try:
                recovery_function = self.recovery_strategies[error_type]
                return recovery_function(error, context)
            except Exception as recovery_error:
                self.logger.error(f"Recovery strategy failed: {recovery_error}")
                return False, None
        
        # No specific recovery strategy available
        return False, None
    
    def _get_user_friendly_message(self, error: Exception, context: Optional[str]) -> str:
        """
        Generate user-friendly error message.
        
        Args:
            error (Exception): The error to describe
            context (Optional[str]): Context where the error occurred
            
        Returns:
            str: User-friendly error message
        """
        if isinstance(error, PreprocessingPipelineError):
            message = error.get_user_message()
        else:
            message = str(error)
        
        if context:
            return f"An error occurred during {context}:\n\n{message}"
        else:
            return f"An error occurred:\n\n{message}"
    
    # Recovery strategy implementations
    
    def _recover_file_operation_error(self, error: FileOperationError, 
                                    context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for file operation errors."""
        # For file not found errors, we can't automatically recover
        # But we can provide helpful suggestions
        if "not found" in error.message.lower():
            self.logger.info("File not found - user intervention required")
            return False, None
        
        # For permission errors, suggest checking permissions
        if "permission" in error.message.lower():
            self.logger.info("Permission error - user intervention required")
            return False, None
        
        # For other file errors, no automatic recovery possible
        return False, None
    
    def _recover_data_validation_error(self, error: DataValidationError,
                                     context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for data validation errors."""
        # Data validation errors typically require user intervention
        # We can provide suggestions but can't automatically fix the data
        self.logger.info("Data validation error - user intervention required")
        return False, None
    
    def _recover_configuration_error(self, error: ConfigurationError,
                                   context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for configuration errors."""
        # Configuration errors require user to provide correct input
        # We can suggest valid ranges but can't automatically fix
        self.logger.info("Configuration error - user input required")
        return False, None
    
    def _recover_preprocessor_error(self, error: PreprocessorError,
                                  context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for preprocessor errors."""
        # If a specific preprocessor fails, we could potentially skip it
        # But this would change the intended processing, so we don't auto-recover
        self.logger.info("Preprocessor error - manual intervention required")
        return False, None
    
    def _recover_processing_error(self, error: ProcessingError,
                                context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for processing errors."""
        # Processing errors are typically logged and the combination is marked as failed
        # This is the expected behavior, so we consider it "recovered"
        self.logger.info("Processing error handled - combination marked as failed")
        return True, "combination_failed"
    
    def _recover_output_error(self, error: OutputError,
                            context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for output errors."""
        # For output errors, we might be able to retry with a different filename
        # But this requires more context, so we don't auto-recover
        self.logger.info("Output error - manual intervention required")
        return False, None
    
    def _recover_resource_error(self, error: ResourceError,
                              context: Optional[str]) -> Tuple[bool, Optional[Any]]:
        """Recovery strategy for resource errors."""
        # For memory errors, we could suggest using memory-efficient processing
        if error.resource_type == "memory":
            self.logger.info("Memory error - suggest using memory-efficient processing")
            return False, "use_memory_efficient_processing"
        
        return False, None
    
    def get_error_summary(self) -> str:
        """
        Get a summary of all errors encountered.
        
        Returns:
            str: Formatted error summary
        """
        if not self.error_history:
            return "No errors recorded."
        
        summary_lines = [f"Error Summary ({len(self.error_history)} errors):"]
        
        # Group errors by type
        error_counts = {}
        for error_info in self.error_history:
            error_type = error_info['error_type']
            error_counts[error_type] = error_counts.get(error_type, 0) + 1
        
        # Add error type counts
        for error_type, count in error_counts.items():
            summary_lines.append(f"  {error_type}: {count}")
        
        # Add recent errors
        summary_lines.append("\nRecent errors:")
        for error_info in self.error_history[-5:]:  # Last 5 errors
            timestamp = time.strftime('%H:%M:%S', time.localtime(error_info['timestamp']))
            context = error_info['context'] or 'unknown'
            summary_lines.append(
                f"  [{timestamp}] {error_info['error_type']} in {context}: "
                f"{error_info['error_message'][:100]}..."
            )
        
        return "\n".join(summary_lines)
    
    def clear_error_history(self):
        """Clear the error history."""
        self.error_history.clear()
        self.logger.info("Error history cleared")


# Decorator functions for error handling

def handle_file_operations(recovery_manager: Optional[ErrorRecoveryManager] = None):
    """
    Decorator to handle file operation errors.
    
    Args:
        recovery_manager: Optional recovery manager instance
        
    Returns:
        Decorated function with file error handling
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except (FileNotFoundError, PermissionError, OSError) as e:
                file_error = FileOperationError(
                    str(e),
                    operation=func.__name__,
                    details=f"Function: {func.__name__}"
                )
                
                if recovery_manager:
                    success, result = recovery_manager.handle_error(
                        file_error, f"file operation: {func.__name__}"
                    )
                    if success:
                        return result
                
                raise file_error
            except Exception as e:
                if not isinstance(e, PreprocessingPipelineError):
                    file_error = FileOperationError(
                        f"Unexpected error in file operation: {str(e)}",
                        operation=func.__name__,
                        details=f"Original error: {type(e).__name__}"
                    )
                    
                    if recovery_manager:
                        recovery_manager.handle_error(
                            file_error, f"file operation: {func.__name__}"
                        )
                    
                    raise file_error
                raise
        
        return wrapper
    return decorator


def handle_data_operations(recovery_manager: Optional[ErrorRecoveryManager] = None):
    """
    Decorator to handle data operation errors.
    
    Args:
        recovery_manager: Optional recovery manager instance
        
    Returns:
        Decorated function with data error handling
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except (ValueError, TypeError, KeyError) as e:
                data_error = DataValidationError(
                    str(e),
                    validation_type=func.__name__,
                    details=f"Function: {func.__name__}"
                )
                
                if recovery_manager:
                    success, result = recovery_manager.handle_error(
                        data_error, f"data operation: {func.__name__}"
                    )
                    if success:
                        return result
                
                raise data_error
            except Exception as e:
                if not isinstance(e, PreprocessingPipelineError):
                    data_error = DataValidationError(
                        f"Unexpected error in data operation: {str(e)}",
                        validation_type=func.__name__,
                        details=f"Original error: {type(e).__name__}"
                    )
                    
                    if recovery_manager:
                        recovery_manager.handle_error(
                            data_error, f"data operation: {func.__name__}"
                        )
                    
                    raise data_error
                raise
        
        return wrapper
    return decorator


@contextmanager
def error_context(context_name: str, recovery_manager: Optional[ErrorRecoveryManager] = None):
    """
    Context manager for handling errors in a specific context.
    
    Args:
        context_name (str): Name of the context for error reporting
        recovery_manager: Optional recovery manager instance
        
    Yields:
        None
    """
    try:
        yield
    except PreprocessingPipelineError as e:
        if recovery_manager:
            success, result = recovery_manager.handle_error(e, context_name)
            if not success:
                raise
        else:
            raise
    except Exception as e:
        # Convert unexpected errors to pipeline errors
        pipeline_error = PreprocessingPipelineError(
            f"Unexpected error in {context_name}: {str(e)}",
            details=f"Original error: {type(e).__name__}",
            error_code="UNEXPECTED_ERROR"
        )
        
        if recovery_manager:
            recovery_manager.handle_error(pipeline_error, context_name)
        
        raise pipeline_error


def retry_on_failure(max_attempts: int = 3, delay: float = 1.0,
                    backoff_factor: float = 2.0,
                    exceptions: Tuple[type, ...] = (Exception,)):
    """
    Decorator to retry function execution on failure.
    
    Args:
        max_attempts (int): Maximum number of retry attempts
        delay (float): Initial delay between retries in seconds
        backoff_factor (float): Factor to multiply delay by after each failure
        exceptions (Tuple[type, ...]): Exception types to retry on
        
    Returns:
        Decorated function with retry logic
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            current_delay = delay
            last_exception = None
            
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    
                    if attempt < max_attempts - 1:  # Not the last attempt
                        logging.getLogger(__name__).warning(
                            f"Attempt {attempt + 1} failed for {func.__name__}: {e}. "
                            f"Retrying in {current_delay:.1f}s..."
                        )
                        time.sleep(current_delay)
                        current_delay *= backoff_factor
                    else:
                        logging.getLogger(__name__).error(
                            f"All {max_attempts} attempts failed for {func.__name__}"
                        )
            
            # All attempts failed, raise the last exception
            raise last_exception
        
        return wrapper
    return decorator


class GracefulShutdownManager:
    """
    Manages graceful shutdown of operations when errors occur.
    
    This class helps ensure that resources are properly cleaned up
    and partial results are saved when operations need to be terminated.
    """
    
    def __init__(self):
        """Initialize the graceful shutdown manager."""
        self.shutdown_callbacks: List[Callable[[], None]] = []
        self.cleanup_callbacks: List[Callable[[], None]] = []
        self.is_shutting_down = False
    
    def register_shutdown_callback(self, callback: Callable[[], None]):
        """
        Register a callback to be called during shutdown.
        
        Args:
            callback: Function to call during shutdown
        """
        self.shutdown_callbacks.append(callback)
    
    def register_cleanup_callback(self, callback: Callable[[], None]):
        """
        Register a callback for resource cleanup.
        
        Args:
            callback: Function to call for cleanup
        """
        self.cleanup_callbacks.append(callback)
    
    def initiate_shutdown(self, reason: str = "Error occurred"):
        """
        Initiate graceful shutdown process.
        
        Args:
            reason (str): Reason for shutdown
        """
        if self.is_shutting_down:
            return
        
        self.is_shutting_down = True
        logger = logging.getLogger(__name__)
        logger.info(f"Initiating graceful shutdown: {reason}")
        
        # Execute shutdown callbacks
        for callback in self.shutdown_callbacks:
            try:
                callback()
            except Exception as e:
                logger.error(f"Error in shutdown callback: {e}")
        
        # Execute cleanup callbacks
        for callback in self.cleanup_callbacks:
            try:
                callback()
            except Exception as e:
                logger.error(f"Error in cleanup callback: {e}")
        
        logger.info("Graceful shutdown completed")
    
    @contextmanager
    def shutdown_context(self, reason: str = "Context error"):
        """
        Context manager that initiates shutdown on error.
        
        Args:
            reason (str): Reason for potential shutdown
            
        Yields:
            None
        """
        try:
            yield
        except Exception as e:
            self.initiate_shutdown(f"{reason}: {e}")
            raise